﻿using System;
using System.Linq;
using System.ServiceModel;
using System.Threading;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;

namespace MultiViewer.Network 
{
    [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single,
        ConcurrencyMode=ConcurrencyMode.Multiple)]
    public class ServerNode : Node, IServerNode
    {
        private IClientNode[] clients;

        private ServiceHost host;

        public ServerNode(params string[] args)
        {
            string ncs = GetParameter("multiViewerNodeCount","mvnodes",args);
            if (!Int32.TryParse(ncs, out nodeCount))
                throw new Exception("Node count is not specified neither in config nor in command line args");
            if (nodeCount < 1)
                throw new Exception("Node count must be positive");
            clients = new IClientNode[nodeCount - 1];
            host = new ServiceHost(this);
            host.Open();
            inited = true;
        }

        public override int[] ConnectedNodes
        {
            get
            {
                lock (clients)
                {
                    List<int> result = new List<int>();
                    result.Add(0); // Server (self) is surely connected
                    for (int i = 0; i < clients.Length; i++)
                        if (clients[i] != null)
                            result.Add(i + 1);
                    return result.ToArray();
                }
            }
        }

        public int Connect(int nodeId)
        {
            if (nodeId < 1 || nodeId >= nodeCount)
                throw new Exception("Node id is out of range");
            if (clients[nodeId - 1] != null)
                throw new Exception("Node id is already used");
            lock (clients)
            {
                clients[nodeId - 1] = OperationContext.Current.GetCallbackChannel<IClientNode>();
                OnNodeConnected(nodeId);
                return nodeCount;
            }
        }

        public void Disconnect(int nodeId)
        {
            if (nodeId < 1 || nodeId >= nodeCount)
                throw new Exception("Node id is out of range");
            if (clients[nodeId - 1] == null)
                throw new Exception("Node id is already used");
            lock (clients)
            {
                clients[nodeId - 1] = null;
                OnNodeDisconnected(nodeId);
            }
        } 

        public override Response[] Send(int[] nodes, Message message)
        {            
            Response[] results = new Response[nodes.Length];
            AutoResetEvent[] completed = new AutoResetEvent[nodes.Length];
            for (int i = 0; i < nodes.Length; i++)
            {
                completed[i] = new AutoResetEvent(false);
                ThreadPool.QueueUserWorkItem(obj =>
                {
                    int idx = (int)obj;
                    results[idx] = Send(nodes[idx], message);
                    completed[idx].Set();
                }, i);
            }
            foreach (WaitHandle wh in completed)
                wh.WaitOne();
            // Wait.WaitAll(completed) doesn't work on STA thread
            return results;
        }

        private Response Send(int node, Message request)
        {
            if (node > 0 && clients[node - 1] == null)
                return new Response(new Exception(String.Format("Node {0} is not connected", node)));
            else
                try
                {
                    object result = 0;
                    if (node == 0)
                        result = HandleMessage(request);
                    else
                        result = clients[node-1].OnMessage(request);
                    return new Response(result);
                }
                catch (Exception exc)
                {
                    return new Response(exc);
                }
        }

        public override void Notify(int[] nodes, Message request)
        {
            IClientNode[] clients = (IClientNode[])this.clients.Clone();
            foreach (int node in nodes)
                ThreadPool.QueueUserWorkItem(obj =>
                {
                    int n = (int)obj;
                    try
                    {
                        if (n == 0)
                            HandleNotification(request);
                        else if(clients[n - 1] != null)
                            clients[n - 1].OnNotification(request);
                    }
                    catch (Exception exc)
                    {
                        Trace.WriteLine("Exception in Notify: " + exc.Message);
                    }
                }, node);
        }

        public override void Perform(Message request)
        {
            ThreadPool.QueueUserWorkItem(dummy =>
            {
                // Unique transaction id
                string id = Guid.NewGuid().ToString();
                //Trace.WriteLine(String.Format("Node {0} initiated transaction T-{1}", request.Sender, id));


                // Local immutable array of clients
                IClientNode[] clients = (IClientNode[])this.clients.Clone();

                bool[] performResults = new bool[clients.Length + 1];
                AutoResetEvent[] completed = new AutoResetEvent[clients.Length + 1];

                // Perform prepare phase on server
                completed[0] = new AutoResetEvent(false);
                ThreadPool.QueueUserWorkItem(dummy2 =>
                {
                  //  Trace.WriteLine(String.Format("T-{0}: preparing node 0 (server)", id));
                    performResults[0] = Prepare(null, id, request);
                    //Trace.WriteLine(String.Format("T-{0}: node 0 (server) returns {1}", id, performResults[0]));
                    completed[0].Set();
                });

                // Perform prepare phase on clients
                for (int i = 0; i < clients.Length; i++)
                {
                    completed[i + 1] = new AutoResetEvent(false);
                    ThreadPool.QueueUserWorkItem(obj =>
                    {
                        int idx = (int)obj;
                      //  Trace.WriteLine(String.Format("T-{0}: preparing node {1}", id, idx + 1));
                        performResults[idx + 1] = Prepare(clients[idx], id, request);
                        //Trace.WriteLine(String.Format("T-{0}: node {1} returns {2}", id, idx + 1, performResults[0]));
                        completed[idx + 1].Set();
                    }, i);
                }

                // Wait for results
                //foreach (WaitHandle wh in completed)
                for (int i = 0; i < completed.Length; i++)
                    completed[i].WaitOne();
                // WaitHandle.WaitAll(completed) doesn't work on STA thread

                bool failure = performResults.Contains(false);
                ThreadPool.QueueUserWorkItem(dummy2 =>
                {
                    try
                    {
                        if (failure)
                        {
                            //Thread.Sleep(100);
                            HandleRollback(id);
                        }
                        else
                        {
                            //Thread.Sleep(100);
                            HandleCommit(id);
                        }
                    }
                    catch (Exception exc)
                    {
                        Trace.WriteLine("Exception on server node when finalizing transaction");
                    }
                });
                for (int i = 0; i < clients.Length; i++)
                    ThreadPool.QueueUserWorkItem(client =>
                    {
                        try
                        {
                            IClientNode c = (IClientNode)client;
                            if (failure)
                                c.OnRollback(id);
                            else
                                c.OnCommit(id);
                        }
                        catch (Exception exc)
                        {
                            Trace.WriteLine("Exception on client node when finalizing transaction");
                        }
                    }, clients[i]);
            });
        }


        private bool Prepare(IClientNode client, string id, Message request)
        {
            try
            {
                if (client != null)
                    return client.OnPrepare(id, request);
                else
                    return HandlePrepare(id, request);
            }
            catch (Exception exc)
            {
                Trace.WriteLine("Transaction failed on some nodes: " + exc.Message);
                return false;
            }
        }
    }
}